// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.util.concurrency;

import com.intellij.concurrency.ThreadContext;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments;
import com.intellij.util.ui.EDT;
import org.jetbrains.annotations.ApiStatus.Internal;
import org.jetbrains.annotations.ApiStatus.Obsolete;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;

import java.awt.*;

/**
 * This class contains various threading assertions.
 * Calls to these functions are generated by {@link org.jetbrains.jps.devkit.threadingModelHelper.TMHInstrumenter TMHInstrumenter}
 * if a function is annotated with a respective annotation and annotation's {@code generateAssertion} is set to {@code true}.
 * These functions are also allowed to be used directly.
 */
public final class ThreadingAssertions {

  private ThreadingAssertions() { }

  private static @NotNull Logger getLogger() {
    return Logger.getInstance(ThreadingAssertions.class);
  }

  private static final String DOCUMENTATION_URL = "https://jb.gg/ij-platform-threading";

  @Internal
  @VisibleForTesting
  public static final String MUST_EXECUTE_IN_READ_ACTION =
    "Read access is allowed from inside read-action only (see Application.runReadAction())";
  private static final String MUST_EXECUTE_IN_READ_ACTION_EXPLICIT =
    "Access is allowed with explicit read lock.\n" +
    "Now each coroutine scheduled on EDT wrapped in implicit write intent lock (which implies read lock too). This implicit lock will be removed in future releases.\n" +
    "Please, use explicit lock API like ReadAction.run(), WriteIntentReadAction.run(), readAction() or writeIntentReadAction() to wrap code which needs lock to access model or PSI.\n" +
    "Please note, that read action API can re-schedule your code to background threads, if you are sure that your code need to be executed on EDT, you need to use write intent read action.\n" +
    "Also, consult with " + DOCUMENTATION_URL;
  @Internal
  @VisibleForTesting
  public static final String MUST_NOT_EXECUTE_IN_READ_ACTION =
    "Must not execute inside read action";
  private static final String MUST_EXECUTE_IN_WRITE_INTENT_READ_ACTION =
    "Access is allowed from write thread only";
  private static final String MUST_EXECUTE_IN_WRITE_INTENT_READ_ACTION_EXPLICIT =
    "Access is allowed from EDT with explicit write intent lock.\n" +
    "Now each coroutine scheduled on EDT wrapped in implicit write intent lock. This implicit lock will be removed in future releases.\n" +
    "Please, use explicit lock API like WriteIntentReadAction.run() or writeIntentReadAction() to wrap code which needs lock to modify model or PSI.\n" +
    "Also, consult with " + DOCUMENTATION_URL;
  @Internal
  @VisibleForTesting
  public static final String MUST_EXECUTE_IN_WRITE_ACTION =
    "Write access is allowed inside write-action only (see Application.runWriteAction())";
  @Internal
  @VisibleForTesting
  public static final String MUST_EXECUTE_IN_EDT =
    "Access is allowed from Event Dispatch Thread (EDT) only";
  @Internal
  @VisibleForTesting
  public static final String MUST_NOT_EXECUTE_IN_EDT =
    "Access from Event Dispatch Thread (EDT) is not allowed";

  private static boolean implicitLock = false;

  /**
   * Asserts that the current thread is the event dispatch thread.
   *
   * @see com.intellij.util.concurrency.annotations.RequiresEdt
   */
  public static void assertEventDispatchThread() {
    if (!EDT.isCurrentThreadEdt()) {
      throwThreadAccessException(MUST_EXECUTE_IN_EDT);
    }
  }

  /**
   * Asserts that the current thread is the event dispatch thread <b>without throwing</b> an exception.
   *
   * @see com.intellij.util.concurrency.annotations.RequiresEdt
   */
  @Obsolete
  public static void softAssertEventDispatchThread() {
    if (!EDT.isCurrentThreadEdt()) {
      getLogger().error(createThreadAccessException(MUST_EXECUTE_IN_EDT));
    }
  }

  /**
   * Asserts that the current thread is <b>not</b> the event dispatch thread.
   *
   * @see com.intellij.util.concurrency.annotations.RequiresBackgroundThread
   */
  public static void assertBackgroundThread() {
    if (EDT.isCurrentThreadEdt()) {
      throwThreadAccessException(MUST_NOT_EXECUTE_IN_EDT);
    }
  }

  /**
   * Asserts that the current thread is <b>not</b> the event dispatch thread <b>without throwing</b> an exception.
   *
   * @see com.intellij.util.concurrency.annotations.RequiresBackgroundThread
   */
  @Obsolete
  public static void softAssertBackgroundThread() {
    if (EDT.isCurrentThreadEdt()) {
      getLogger().error(createThreadAccessException(MUST_NOT_EXECUTE_IN_EDT));
    }
  }

  /**
   * Asserts that the current thread has read access.
   * <p/>
   * For consistency with other assertions, this function <b>throws</b> an error when called without holding the read lock.
   *
   * @see #softAssertReadAccess
   * @see com.intellij.util.concurrency.annotations.RequiresReadLock
   */
  public static void assertReadAccess() {
    if (!ApplicationManager.getApplication().isReadAccessAllowed()) {
      throwThreadAccessException(MUST_EXECUTE_IN_READ_ACTION);
    }
    else if (isImplicitLockOnEDT() && !ApplicationManager.getApplication().isUnitTestMode()) {
      reportImplicitRead();
    }
  }

  /**
   * Reports message about implicit read to logger at error level
   */
  @Internal
  public static void reportImplicitRead() {
    getLogger().error(new RuntimeExceptionWithAttachments(MUST_EXECUTE_IN_READ_ACTION_EXPLICIT));
  }

  /**
   * Asserts that the current thread has read access <b>without throwing</b> an exception.
   * <p/>
   * Historically, it was not possible to throw everywhere,
   * so this function logs an error without throwing, but then proceeds normally.
   * When writing the new code, please prefer throwing {@link #assertReadAccess} instead,
   * because only it can guarantee that the caller holds the read lock.
   *
   * @see com.intellij.util.concurrency.annotations.RequiresReadLock
   */
  @Obsolete
  public static void softAssertReadAccess() {
    if (!ApplicationManager.getApplication().isReadAccessAllowed()) {
      getLogger().error(createThreadAccessException(MUST_EXECUTE_IN_READ_ACTION));
    }
  }

  /**
   * Asserts that the current thread has <b>no</b> read access.
   *
   * @see com.intellij.util.concurrency.annotations.RequiresReadLockAbsence
   */
  public static void assertNoReadAccess() {
    if (ApplicationManager.getApplication().isReadAccessAllowed()) {
      throwThreadAccessException(MUST_NOT_EXECUTE_IN_READ_ACTION);
    }
  }

  /**
   * Asserts that the current thread has write-intent read access.
   */
  public static void assertWriteIntentReadAccess() {
    if (!ApplicationManager.getApplication().isWriteIntentLockAcquired()) {
      throwWriteIntentReadAccess();
    }
    else if (isImplicitLockOnEDT()) {
      reportImplicitWriteIntent();
    }
  }

  /**
   * Reports message about implicit read to logger at error level
   */
  @Internal
  public static void reportImplicitWriteIntent() {
    getLogger().error(new RuntimeExceptionWithAttachments(MUST_EXECUTE_IN_WRITE_INTENT_READ_ACTION_EXPLICIT));
  }

  /**
   * Throw error that current thread hasn't write-intent read access.
   */
  public static void throwWriteIntentReadAccess() {
    throwThreadAccessException(MUST_EXECUTE_IN_WRITE_INTENT_READ_ACTION);
  }

  /**
   * Asserts that the current thread has write access.
   *
   * @see com.intellij.util.concurrency.annotations.RequiresWriteLock
   */
  public static void assertWriteAccess() {
    if (!ApplicationManager.getApplication().isWriteAccessAllowed()) {
      throwThreadAccessException(MUST_EXECUTE_IN_WRITE_ACTION);
    }
  }

  private static void throwThreadAccessException(@NotNull @NonNls String message) {
    throw createThreadAccessException(message);
  }

  private static @NotNull RuntimeExceptionWithAttachments createThreadAccessException(@NonNls @NotNull String message) {
    // Don't suggest Read Action on EDT with coroutines, as it rescheduled code
    boolean skipReadAction = EDT.isCurrentThreadEdt() && ThreadContext.currentThreadContextOrNull() != null;
    return new RuntimeExceptionWithAttachments(
      message + "; If you access or modify model on EDT consider wrapping your code in WriteIntentReadAction " +
      (skipReadAction ? "" : " or ReadAction") +
      "; see " + DOCUMENTATION_URL + " for details" + "\n" + getThreadDetails()
    );
  }

  private static @NotNull String getThreadDetails() {
    Thread current = Thread.currentThread();
    Thread edt = EDT.getEventDispatchThreadOrNull();
    return "Current thread: " + describe(current) + " (EventQueue.isDispatchThread()=" + EventQueue.isDispatchThread() + ")\n" +
           "SystemEventQueueThread: " + (edt == current ? "(same)" : describe(edt));
  }

  private static @NotNull String describe(@Nullable Thread o) {
    return o == null ? "null" : o + " " + System.identityHashCode(o);
  }

  public static boolean isImplicitLockOnEDT() {
    if (!EDT.isCurrentThreadEdt())
      return false;
    Application app = ApplicationManager.getApplication();
    // Don't report implicit locks for unit tests, too many false positives
    if (app != null && app.isUnitTestMode())
      return false;
    // Reset to decrease noise
    boolean il = implicitLock;
    implicitLock = false;
    return il;
  }

  public static void setImplicitLockOnEDT(boolean implicitLock) {
    if (!EDT.isCurrentThreadEdt())
      return;
    ThreadingAssertions.implicitLock = implicitLock;
  }
}
